package cn.sciento.boot.api.customize.service.impl;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import cn.sciento.core.exception.CommonException;
import javassist.ClassClassPath;
import javassist.ClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
import javax.validation.constraints.NotNull;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import cn.sciento.boot.api.customize.commons.annotation.MethodDescription;
import cn.sciento.boot.api.customize.commons.vo.MethodMetaData;
import cn.sciento.boot.api.customize.service.ApiScanService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.data.type.classreading.MethodsMetadataReaderFactory;
import org.springframework.util.Assert;

public class DefaultApiScanService implements ApiScanService, ResourceLoaderAware, InitializingBean {
  private static final Logger LOGGER = LoggerFactory.getLogger(DefaultApiScanService.class);
  
  private ResourceLoader resourceLoader;
  
  private ResourcePatternResolver resolver;
  
  private MetadataReaderFactory metadataReader;
  
  public List<MethodMetaData> scanMethod(List<String> packageNames) {
    List<MethodMetaData> methodMetaDataList = new ArrayList<>(1024);
    if (CollectionUtils.isEmpty(packageNames))
      return Collections.emptyList(); 
    for (String packageName : packageNames)
      methodMetaDataList.addAll(scanMethod(packageName)); 
    return methodMetaDataList;
  }
  
  public List<MethodMetaData> scanMethod(String packageName) {
    Assert.notNull(packageName, "packageName should not be null.");
    LOGGER.debug("customize parse package [{}]", packageName);
    List<MethodMetaData> methodMetaDataList = new ArrayList<>(512);
    String packagePattern = packageName.replaceAll("\\.", "/");
    try {
      Resource[] resources = this.resolver.getResources("classpath*:" + packagePattern + "/**/*.class");
      ClassPool classPool = new ClassPool(true);
      classPool.insertClassPath((ClassPath)new ClassClassPath(getClass()));
      for (Resource resource : resources) {
        MetadataReader reader = this.metadataReader.getMetadataReader(resource);
        ClassMetadata classMetadata = reader.getClassMetadata();
        parseClassMethodMetaData(classPool, classMetadata, methodMetaDataList);
      } 
    } catch (IOException e) {
      LOGGER.error("parse class method error, IOException, ex = {}", e.getMessage(), e);
      throw new CommonException("customize.parseClassMethodError.IOException， ex = " + e.getMessage());
    } catch (NotFoundException e) {
      LOGGER.error("parse class method error, NotFoundException, ex = {}", e.getMessage(), e);
      throw new CommonException("customize.parseClassMethodError.NotFoundException，ex = " + e.getMessage());
    } 
    return methodMetaDataList;
  }
  
  private void parseClassMethodMetaData(ClassPool classPool, ClassMetadata classMetadata, List<MethodMetaData> methodMetaDataList) throws NotFoundException {
    CtClass ctClass = classPool.get(classMetadata.getClassName());
    if (ctClass.isInterface() || ctClass.isAnnotation() || ctClass.isEnum() || ctClass.isPrimitive() || ctClass.isArray() || ctClass.getSimpleName().contains("$")) {
      LOGGER.info("skip interface/annotation/enum/primitive/array class type or class is proxy class, className = {}", ctClass.getName());
      return;
    } 
    try {
      CtMethod[] methods = ctClass.getDeclaredMethods();
      for (CtMethod method : methods) {
        if (method.getName().contains("$")) {
          LOGGER.info("skip proxy method, className = {}, method = {}", ctClass.getSimpleName(), method.getName());
        } else {
          MethodDescription descAnnotation = (MethodDescription)method.getAnnotation(MethodDescription.class);
          String description = (descAnnotation != null) ? descAnnotation.value() : null;
          MethodMetaData metaData = new MethodMetaData(ctClass.getPackageName(), ctClass.getSimpleName(), method.getName(), parseMethodParameter(method), description);
          methodMetaDataList.add(metaData);
        } 
      } 
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      ctClass.detach();
    } 
  }
  
  private String parseMethodParameter(CtMethod method) {
    String signature = StringUtils.substringBetween(method.getLongName(), "(", ")");
    return (String)StringUtils.defaultIfBlank(signature, null);
  }
  
  public void setResourceLoader(@NotNull ResourceLoader resourceLoader) {
    this.resourceLoader = resourceLoader;
  }
  
  public void afterPropertiesSet() throws Exception {
    Assert.notNull(this.resourceLoader, "resourceLoader should not be null");
    this.resolver = ResourcePatternUtils.getResourcePatternResolver(this.resourceLoader);
    this.metadataReader = (MetadataReaderFactory)new MethodsMetadataReaderFactory(this.resourceLoader);
  }
}
